﻿using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Red.Core.Network.PacketNetwork
{
    public delegate void DisconnectedHandler( int connectionId );

    internal class IDAllocator
    {
        private int _lastUsedId = 1;

        public int AllocateID()
        {
            return Interlocked.Increment( ref _lastUsedId );
        }
    }

    public interface IRawConnectionInterface
    {
        void OnDisconnected( int connectionID );
        void OnData( byte[] data, int size, int connectionID );
    }

    public interface IRawListenerInterface
    {
        void OnClosed( int listenerID );
        bool OnConnection( int connectionID, out IRawConnectionInterface outConnectionInterface );
    }

    public interface IRawConnectionsManager
    {
        int CreateListener( CommAddress localAddress, IRawListenerInterface listenerCallback );
        void CloseListener( int listenerId );

        Task<int> CreateConnection( CommAddress address, IRawConnectionInterface callback );
        void CloseConnection( int connectionId );

        void SendAsync( int connectionId, byte[] data );

        event DisconnectedHandler Disconnected;
    }

    public class PacketNetworkManager
    {
        private IRawConnectionsManager _network;
        private IDAllocator _idAllocator = new IDAllocator();
        private ConcurrentDictionary<int, PacketListener> _listeners = new ConcurrentDictionary<int, PacketListener>();
        private PacketConnetionCollection _connections = new PacketConnetionCollection();

        private ConcurrentDictionary<int, ulong> _connectionsTotalBytesSent = new ConcurrentDictionary<int, ulong>();

        public PacketNetworkManager( IRawConnectionsManager network )
        {
            _network = network;
            _network.Disconnected += _network_Disconnected;
        }

        public int CreateListener( CommAddress localAddress, IPacketConnectionListener callback )
        {
            int id = _idAllocator.AllocateID();
            PacketListener listener = new PacketListener( id, _connections, _idAllocator, callback );

            listener.AssignedRawListenerID = _network.CreateListener( localAddress, listener );

            if ( listener.AssignedRawListenerID == 0 )
            {
                return 0;
            }

            _listeners.AddOrUpdate( id, listener, ( key, oldValue ) => listener );

            Debug.WriteLine( "[PacketNetwork] Listener created." );

            return id;
        }

        public void CloseListener( int packetListenerID )
        {
            PacketListener listener;
            bool isRemoved = _listeners.TryRemove( packetListenerID, out listener );
            if ( isRemoved )
            {
                _connections.RemoveConnectionsRelatedToListener( packetListenerID,
                ( PacketConnection connection ) =>
                {
                    _network.CloseConnection( connection.RawConnectionID );
                } );

                _network.CloseListener( listener.AssignedRawListenerID );
            }
        }

        public async Task<int> CreateConnection( CommAddress address, IPacketConnectionListener callback )
        {
            int id = _idAllocator.AllocateID();
            PacketConnection connection = new PacketConnection( id, callback );
            connection.RawConnectionID = await _network.CreateConnection( address, connection );

            if ( connection.RawConnectionID == 0 )
            {
                return 0;
            }

            _connections.AddConnection( id, connection );

            Debug.WriteLine( $"[PacketNetwork] Connection created: {id}" );

            return id;
        }

        public void CloseConnection( int packetConnectionID )
        {
            PacketConnetionCollection.OnRemoveConnectionHandler handler = ( PacketConnection connection ) =>
            {
                _network.CloseConnection( connection.RawConnectionID );
                Debug.WriteLine( $"[PacketNetwork] Connection closed: {connection.RawConnectionID}" );
            };

            _connections.RemoveConnection( packetConnectionID, handler );
        }

        private void _network_Disconnected( int connectionId )
        {
            _connections.RemoveConnectionByRawId( connectionId );
        }

        public ulong GetTotalBytesReceivedForConnection( int packetConnectionID )
        {
            PacketConnection connection;
            if ( _connections.FindConnection( packetConnectionID, out connection ) )
            {
                return connection.TotalBytesReceived;
            }

            return 0;
        }

        public ulong GetTotalBytesReceivedForListener( int packetListenerID )
        {
            PacketListener listener;
            if ( _listeners.TryGetValue( packetListenerID, out listener ) )
            {
                return listener.TotalBytesReceived;
            }

            return 0;
        }

        public ulong GetTotalBytesSentForConnection( int packetConnectionID )
        {
            ulong totalBytesSent = 0;
            if ( _connectionsTotalBytesSent.TryGetValue( packetConnectionID, out totalBytesSent ) )
                return totalBytesSent;
            return 0;
        }

        public void Send( int packetConnectionID, MemoryStream packetContent )
        {
            PacketConnection connection;
            bool connectionFound = _connections.FindConnection( packetConnectionID, out connection );

            if ( connectionFound )
            {
                MemoryStream packet = PacketCreator.CreatePacket( packetContent );
                byte[] data = packet.ToArray();
                ulong bytesSent = (ulong)data.LongLength;
                _network.SendAsync( connection.RawConnectionID, data );
                _connectionsTotalBytesSent.AddOrUpdate( packetConnectionID, bytesSent, ( key, oldValue ) => oldValue + bytesSent );
            }
        }
    }
}
